#region
/* Adoor.NET - Object/Relational mapping framework leveraging ADO.NET`s powerful disconnected mode.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */ 
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;
using System.Data;
using Adoor.Expression;
using Adoor.Expression.AST;
using Adoor.Object.Domain;
using Adoor.Relational;
using Adoor.Object;
using Adoor.Object.Query;
using Adoor.Object.Entity;
using Adoor.Object.Disconnected;

namespace Adoor.Object.Relational
{
	// SingleLookupTableMapping & SingleLookupTableFactory  are just like
	// SingleReferenceMappingWrapper(LookupTableMapping) & SingleReferenceFactoryWrapper(LookupTableFactory)
	// are they useful ? (the only Fetch avoids creating a one-element enumerator...)

	// TODO: share ancestor with LookupTableFactory
    public class SingleLookupTableMapping<TReference> :
        ISingleReferenceReader<TReference>
        , IReferenceMappingStrategyProvider
        , IReferenceMappingStrategy
	{
		private ReferenceData referenceData;
		public SingleLookupTableMapping(ReferenceData referenceData, string sourceForeignKey, string targetForeignKey, string tableName)
		{
			this.referenceData = referenceData;
			this.tableName = tableName;
			this.sourceForeignKey = sourceForeignKey;
			this.targetForeignKey = targetForeignKey;

			this.ReferenceData.RegisterService(typeof(IReferenceReader), this);
			this.ReferenceData.RegisterService(typeof(ISingleReferenceReader<TReference>), this);
			this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategyProvider), this);
			this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategy), this);
		}

		public ReferenceData ReferenceData
		{
			get { return this.referenceData; }
		}

		private ICollection<DataColumn> dataColumns;
		protected ICollection<DataColumn> DataColumns
		{
			get
			{
				if (this.dataColumns == null)
				{
					IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
					IEntityMappingStrategyProvider targetProvider = (IEntityMappingStrategyProvider)this.ReferenceData.TargetData.GetService(typeof(IEntityMappingStrategyProvider));
					this.dataColumns = new DataColumn[]
					{
						new DataColumn(this.sourceForeignKey, sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2).KeyMappingStrategy.DataColumns[0].DataType), // TODO: remove MappingStrategyEnum.Remote
						new DataColumn(this.targetForeignKey, targetProvider.GetMappingStrategy(MappingStrategyEnum.Remote2).KeyMappingStrategy.DataColumns[0].DataType) // TODO: remove MappingStrategyEnum.Remote
					};
				}
				return this.dataColumns;
			}
		}

		public string SourceForeignKey
		{
			get { return this.sourceForeignKey; } 
		}

		public string TargetForeignKey
		{
			get { return this.targetForeignKey; }
		}

		public string TableName
		{
			get { return this.tableName; }
		}

		public void InsertDataColumn(IList<DataColumn> dataColumns)
		{
			// nothing
		}

		public TReference GetValue(IEntity sourceEntity)
		{
			DataRow row = LoadRow(sourceEntity);
			if (row == null)
				return default(TReference);
			KeyValue targetKey = new KeyValue(row[this.ReferenceData.TargetRole]);
			DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;
			ObjectContext context = storage.Context;
			if (context != null)
				return context.Find<TReference>(this.ReferenceData.TargetData, ((IKeyMappingStrategy)this.ReferenceData.TargetData.OidData).GetOid(targetKey));
			else
				return storage.Find<TReference>(this.ReferenceData.TargetData, ((IKeyMappingStrategy)this.ReferenceData.TargetData.OidData).GetOid(targetKey));
		}

		public IList<TReference> Collect(IEntity sourceEntity)
		{
			TReference targetEntity = GetValue(sourceEntity);
			if (targetEntity == null)
				return new TReference[] {};
			return new TReference[] { targetEntity };
		}

		protected DataRow LoadRow(IEntity sourceEntity)
		{
			// First try : without preloading
			int rowIndex = -1;

			// TODO: remove cast (use generics in C# 2)
			DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;

			DataView index = storage.GetIndex(this.TableName, this.SourceForeignKey);
			if (index != null)
				rowIndex = index.Find(sourceEntity.State.Oid);
			if (rowIndex == -1 && LoadRelation(sourceEntity)) 
			{
				// Second try
				if (index == null)
					index = storage.GetIndex(this.TableName, this.SourceForeignKey);
				if (index != null)
					rowIndex = index.Find(sourceEntity.State.Oid);
			}

			if (rowIndex == -1)
				return null;
			DataRow result = index[rowIndex].Row;
			return result.RowState == DataRowState.Deleted ? null : result;
		}

		public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string correlation, string alias)
		{
			MappingStrategyEnum kind = MappingStrategyEnum.Remote2; // TODO: remove MappingStrategyEnum.Remote

			string temp_alias = correlation + "_" + alias;
			
			IEntityMappingStrategyProvider provider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
			RelationalOperator join1 = new JoinOperator(isOuterJoin, source, 
				provider.GetMappingStrategy(kind).KeyMappingStrategy.PrimaryKey,   
				new ScanQuery(this.TableName, this.DataColumns), this.SourceForeignKey,
				correlation, temp_alias);
			return new JoinOperator(isOuterJoin,
				join1, this.TargetForeignKey,
				targetQuery, targetKey.PrimaryKey,
				temp_alias, alias);
		}

		public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string selector, string alias, bool partial, EntityData sourceEntity)
		{
			throw new NotSupportedException();
		}

		public RelationalOperator Cross(RelationalOperator source, RelationalQuery targetQuery, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
		{
			string temp_alias = correlation + "_" + alias;

			MappingStrategyEnum kind = MappingStrategyEnum.Remote2; // TODO: remove MappingStrategyEnum.Remote
			IEntityMappingStrategyProvider provider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
			RelationalOperator join1 = new JoinOperator(isOuterJoin, source, 
				provider.GetMappingStrategy(kind).KeyMappingStrategy.PrimaryKey,   
				new ScanQuery(this.TableName, this.DataColumns), this.SourceForeignKey,
				correlation, temp_alias);

			return new CrossOperator(join1, targetQuery, alias, joinCondition, isOuterJoin);
		}

		public RelationalOperator Cross(RelationalOperator source, RelationalOperator source2, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
		{
			IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
			IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO: remove MappingStrategyEnum.Remote

			string temp_alias = this.TableName + "_" + alias;

								
			CrossOperator co = null;
			joinCondition = ExpressionReplace.ReplaceNameQualifer(joinCondition, this.ReferenceData.SourceData.EntityType.Name, temp_alias);
			joinCondition = new ExpressionBinary(ScannerToken.And, joinCondition, 
				new ExpressionBinary(ScannerToken.Eq
				,new ExpressionName(temp_alias, this.SourceForeignKey)
				,new ExpressionName(this.ReferenceData.SourceData.EntityType.Name, sourceMappingStrategy.KeyMappingStrategy.PrimaryKey)));
			co = new CrossOperator(source, new AliasOperator( new ScanQuery(this.TableName, this.DataColumns), temp_alias)
				, temp_alias, null,isOuterJoin);
			co = new CrossOperator(co, 
				source2, alias, joinCondition, isOuterJoin);				
			return co;
		}

		public bool LoadRelation(IEntity sourceEntity)
		{
			PreloadFlags flags = PreloadFlags.Reference;
			if (sourceEntity.State.IsReferenceComplete(this.ReferenceData.TargetRole, ref flags))
				return false;

			ObjectPath source = this.ReferenceData.SourceData.GetPath(
                this.ReferenceData.SourceData.EntityType.Name
                ).Where("[" + this.ReferenceData.SourceData.EntityType.Name + "] = ?");

			string temp_alias = this.ReferenceData.SourceData.EntityType.Name + "_" + this.ReferenceData.TargetRole;

			System.Collections.ICollection args = RelationalQueryGenerator.GenerateArgs(new object[] { sourceEntity} );
			RelationalOperator command = RelationalQueryGenerator.GenerateOperator(MappingStrategyEnum.Remote2, source).AsQuery().Rename(temp_alias);

			command = new JoinOperator(false,
				command, "OID",
				new ScanQuery(this.TableName, this.DataColumns), this.SourceForeignKey,
				this.ReferenceData.SourceData.EntityType.Name, temp_alias);
			RelationalQuery rq = command.Project(
				new SelectExpression(this.TargetForeignKey, new ExpressionName(temp_alias, this.TargetForeignKey)),
				new SelectExpression(this.SourceForeignKey, new ExpressionName(temp_alias, this.SourceForeignKey)));
			rq.Distinct = true; // Distinct=true is correct since we only have foreign key columns

			// TODO: remove cast (use generics in C# 2)
			DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;
			ObjectContext context = storage.Context;

			// TODO: remove cast (use generics in C# 2)
            RelationalObjectSource rsource = (RelationalObjectSource)this.ReferenceData.SourceData.Domain.DomainGroup.RemoteSource;
			if (rsource == null)
				throw new Exception("Remote source not found for entity " + this.ReferenceData.SourceData.EntityType.FullName);

			DataTable table = Adoor.Data.DataUtils.CreateCaseInsesitiveDataTable();
			rsource.RelationalSource.Fill(rq, args, table); 

			storage.FillTable(this.TableName, this.DataColumns, new DataView(table));

			sourceEntity.State.CompleteReference(this.ReferenceData.TargetRole, PreloadFlags.Reference);

			return true;
		}

        public System.Collections.IEnumerator EnumTargetKeys(IEntity sourceEntity)
		{
			throw new NotImplementedException();
		}

		public void FetchReference(ObjectContext context, ObjectPath path, object args, string root, string alias, bool isGloballyLoaded)
		{
			throw new NotImplementedException();
		}

        public void FetchTargetEntities(IEntity sourceEntity)
		{
			throw new NotImplementedException();
		}

        public void InitInverseReferenceReader<TInverse>(ReferenceData inverseReferenceData)
		{
			new LookupTableMapping<TInverse>(inverseReferenceData, this.SourceForeignKey, this.TargetForeignKey, this.TableName);
		}

		public IReferenceMappingStrategy GetMappingStrategy(MappingStrategyEnum ms)
		{
			return this;
		}

		public ExpressionNode GetRelationalExpression(string correlation, string value, string alias)
		{
			return new ExpressionName(correlation, value);
		}

		private string tableName;
		private string sourceForeignKey;
		private string targetForeignKey;
	}

    public class SingleLookupTableFactory<TReference> : 
        SingleLookupTableMapping<TReference>
        , ISingleReferenceWriter<TReference>
	{
		public SingleLookupTableFactory(ReferenceData referenceData, string sourceForeignKey, string targetForeignKey, string tableName):
			base(referenceData, sourceForeignKey, targetForeignKey, tableName)
		{
		}

		public SingleLookupTableFactory(ReferenceData referenceData, XmlNode metadata): this(referenceData,
			GetSourceForeignKey(metadata),
			GetTargetForeignKey(metadata),
			metadata.SelectSingleNode("@tableName").Value)
		{
			XmlNode updateCommandNode = metadata.SelectSingleNode("@updateCommand");
			if (updateCommandNode != null)
				this.updateCommand = updateCommandNode.Value;
			
            XmlNode insertCommandNode = metadata.SelectSingleNode("@insertCommand");
			if (insertCommandNode != null)
				this.insertCommand = insertCommandNode.Value;
			
            XmlNode deleteCommandNode = metadata.SelectSingleNode("@deleteCommand");
			if (deleteCommandNode != null)
				this.deleteCommand = deleteCommandNode.Value;
		}

		static string GetSourceForeignKey(XmlNode metadata)
		{
			return metadata.SelectSingleNode("@sourceForeignKey").Value;
		}

		static string GetTargetForeignKey(XmlNode metadata)
		{
			return metadata.SelectSingleNode("@targetForeignKey").Value;
		}

//		static string GetTargetRole(XmlNode metadata)
//		{
//			return metadata.ParentNode.LocalName;
//		}

        public void InitInverseReferenceWriter<TInverse>(ReferenceData inverseReferenceData)
		{
			new LookupTableFactory<TInverse>(inverseReferenceData, this.TargetForeignKey, this.SourceForeignKey, this.TableName); 
		}

		private bool aggregate = false;  // TODO: not used
		public bool Aggregate
		{
			get { return this.aggregate; } 
		}

        public void SetValue(IEntity sourceEntity, IEntity targetEntity)
		{
			if (targetEntity != null && targetEntity.State.Storage != sourceEntity.State.Storage)
				throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name +"'" );				

			DataRow row = LoadRow(sourceEntity);
			
			if (row == null) 
			{
				if (targetEntity == null)
					return;

				// TODO: remove cast (use generics in C# 2)
				DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;
				row = storage.CreateRow(TableName, this.DataColumns);
			}
			row.BeginEdit(); // begin update composite key - try finally not needed since an exception would be fatal
			row[this.SourceForeignKey] = sourceEntity.State.Oid;
			row[this.TargetForeignKey] = targetEntity == null ? DBNull.Value : (object)targetEntity.State.Oid;
			row.EndEdit();   // end update composite key
			// to protect the list from concurrent updates, we mark sourceState as modified
			sourceEntity.State.Touch(); // DataRowState = Modified
		}

		public void Clear(IObjectStorage storage)
		{
			// TODO: remove cast (use generics in C# 2)
			DataSetObjectStorage rstorage = (DataSetObjectStorage)storage;

			DataTable table = rstorage.GetTable(this.TableName, null);
			if (table != null)
				table.Clear();
		}

		public void Update(ObjectContext context)
		{
			// TODO: remove cast (use generics in C# 2)
			DataSetObjectStorage storage = (DataSetObjectStorage)context.LocalStorage;

			DataTable table = storage.GetTable(this.TableName, null);
			if (table != null) 
			{
                RelationalObjectSource orRemote = (RelationalObjectSource)this.ReferenceData.SourceData.Domain.DomainGroup.RemoteSource;
				if (orRemote == null)
					throw new Exception("Remote source not found for entity " + this.ReferenceData.SourceData.EntityType.FullName);

				//				if (table.ExtendedProperties[typeof(IDbTransaction)] != transaction) 
				//				{
				// avoid multiple updates for bi-directional associations
				//					table.ExtendedProperties[typeof(IDbTransaction)] = transaction;
                List<DataColumn> cols = new List<DataColumn>();
                foreach (DataColumn dc in table.Columns)
                {
                    cols.Add(dc);
                }
				orRemote.UpdateTable(table, false,
					this.updateCommand, this.insertCommand, this.deleteCommand, cols);
				//				}
			}
		}

		public void Commit(ObjectContext context)
		{
			// TODO: remove cast (use generics in C# 2)
			DataSetObjectStorage storage = (DataSetObjectStorage)context.LocalStorage;

			DataTable table = storage.GetTable(this.TableName, null);
			if (table != null) 
			{
				table.AcceptChanges();
			}
		}

		public void Cancel(IObjectStorage storage)
		{
			// TODO: remove cast (use generics in C# 2)
			DataSetObjectStorage rstorage = (DataSetObjectStorage)storage;

			DataTable table = rstorage.GetTable(this.TableName, null);
			if (table != null)
				table.RejectChanges();
		}

		public void Remove(IEntity sourceEntity, IEntity targetEntity)
		{
			if (targetEntity.State.Storage != sourceEntity.State.Storage)
				throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name +"'" );
			
//			if (GetValue(sourceEntity) != targetEntity)
//				throw new Exception("Cannot remove entity from the reference");
			if (object.Equals(GetValue(sourceEntity), targetEntity))
				SetValue(sourceEntity, null);
		}

		public void Add(IEntity sourceEntity, IEntity targetEntity, bool checkForIntegrity)
		{
			if (targetEntity != null && targetEntity.State.Storage != sourceEntity.State.Storage)
				throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name +"'" );
			if (checkForIntegrity && GetValue(sourceEntity) != null)
				throw new Exception("Cannot add entity to the reference");
			SetValue(sourceEntity, targetEntity);
		}

       
        public new IList<TReference> Collect(IEntity sourceEntity)
        {
            List<TReference> result = new List<TReference>();
            TReference targetEntity = GetValue(sourceEntity);            
            if (targetEntity != null)
                result.Add(targetEntity);
            return result;
        }

		private string updateCommand;
		private string insertCommand;
		private string deleteCommand;
	}
}
